/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.internal.net;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.BindException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.ServerSocketChannel;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;

import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.Logger;

import org.apache.geode.GemFireConfigException;
import org.apache.geode.SystemConnectException;
import org.apache.geode.SystemFailure;
import org.apache.geode.admin.internal.InetAddressUtil;
import org.apache.geode.cache.wan.GatewaySender;
import org.apache.geode.cache.wan.GatewayTransportFilter;
import org.apache.geode.distributed.ClientSocketFactory;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.DistributionConfigImpl;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.ConnectionWatcher;
import org.apache.geode.internal.GfeConsoleReaderFactory;
import org.apache.geode.internal.GfeConsoleReaderFactory.GfeConsoleReader;
import org.apache.geode.internal.admin.SSLConfig;
import org.apache.geode.internal.cache.wan.TransportFilterServerSocket;
import org.apache.geode.internal.cache.wan.TransportFilterSocketFactory;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LocalizedMessage;
import org.apache.geode.internal.security.SecurableCommunicationChannel;
import org.apache.geode.internal.util.PasswordUtil;

/**
 * Analyze configuration data (gemfire.properties) and configure sockets
 * accordingly for SSL.
 * <p>
 * gemfire.useSSL = (true|false) default false.<br/>
 * gemfire.ssl.debug = (true|false) default false.<br/>
 * gemfire.ssl.needClientAuth = (true|false) default true.<br/>
 * gemfire.ssl.protocols = <i>list of protocols</i><br/>
 * gemfire.ssl.ciphers = <i>list of cipher suites</i><br/>
 * <p>
 * The following may be included to configure the certificates used by the
 * Sun Provider.
 * <p>
 * javax.net.ssl.trustStore = <i>pathname</i><br/>
 * javax.net.ssl.trustStorePassword = <i>password</i><br/>
 * javax.net.ssl.keyStore = <i>pathname</i><br/>
 * javax.net.ssl.keyStorePassword = <i>password</i><br/>
 * <p>
 * Additional properties will be set as System properties to be available
 * as needed by other provider implementations.
 */
public class SocketCreator {

  private static final Logger logger = LogService.getLogger();

  /**
   * Optional system property to enable GemFire usage of link-local addresses
   */
  public static final String USE_LINK_LOCAL_ADDRESSES_PROPERTY = DistributionConfig.GEMFIRE_PREFIX + "net.useLinkLocalAddresses";

  /**
   * True if GemFire should use link-local addresses
   */
  private static final boolean useLinkLocalAddresses = Boolean.getBoolean(USE_LINK_LOCAL_ADDRESSES_PROPERTY);

  /**
   * we cache localHost to avoid bug #40619, access-violation in native code
   */
  private static final InetAddress localHost;

  /**
   * all classes should use this variable to determine whether to use IPv4 or IPv6 addresses
   */
  private static boolean useIPv6Addresses = !Boolean.getBoolean("java.net.preferIPv4Stack") && Boolean.getBoolean("java.net.preferIPv6Addresses");

  private static final Map<InetAddress, String> hostNames = new HashMap<>();

  /**
   * flag to force always using DNS (regardless of the fact that these lookups can hang)
   */
  public static final boolean FORCE_DNS_USE = Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "forceDnsUse");

  /**
   * set this to false to inhibit host name lookup
   */
  public static volatile boolean resolve_dns = true;

  /**
   * True if this SocketCreator has been initialized and is ready to use
   */
  private boolean ready = false;

  /**
   * Only print this SocketCreator's config once
   */
  private boolean configShown = false;

  /**
   * context for SSL socket factories
   */
  private SSLContext sslContext;

  private SSLConfig sslConfig;

  static {
    InetAddress inetAddress = null;
    try {
      inetAddress = InetAddress.getByAddress(InetAddress.getLocalHost().getAddress());
      if (inetAddress.isLoopbackAddress()) {
        InetAddress ipv4Fallback = null;
        InetAddress ipv6Fallback = null;
        // try to find a non-loopback address
        Set myInterfaces = getMyAddresses();
        boolean preferIPv6 = SocketCreator.useIPv6Addresses;
        String lhName = null;
        for (Iterator<InetAddress> it = myInterfaces.iterator(); lhName == null && it.hasNext(); ) {
          InetAddress addr = it.next();
          if (addr.isLoopbackAddress() || addr.isAnyLocalAddress()) {
            break;
          }
          boolean ipv6 = addr instanceof Inet6Address;
          boolean ipv4 = addr instanceof Inet4Address;
          if ((preferIPv6 && ipv6) || (!preferIPv6 && ipv4)) {
            String addrName = reverseDNS(addr);
            if (inetAddress.isLoopbackAddress()) {
              inetAddress = addr;
              lhName = addrName;
            } else if (addrName != null) {
              inetAddress = addr;
              lhName = addrName;
            }
          } else {
            if (preferIPv6 && ipv4 && ipv4Fallback == null) {
              ipv4Fallback = addr;
            } else if (!preferIPv6 && ipv6 && ipv6Fallback == null) {
              ipv6Fallback = addr;
            }
          }
        }
        // vanilla Ubuntu installations will have a usable IPv6 address when
        // running as a guest OS on an IPv6-enabled machine.  We also look for
        // the alternative IPv4 configuration.
        if (inetAddress.isLoopbackAddress()) {
          if (ipv4Fallback != null) {
            inetAddress = ipv4Fallback;
            SocketCreator.useIPv6Addresses = false;
          } else if (ipv6Fallback != null) {
            inetAddress = ipv6Fallback;
            SocketCreator.useIPv6Addresses = true;
          }
        }
      }
    } catch (UnknownHostException e) {
    }
    localHost = inetAddress;
  }

  /**
   * A factory used to create client <code>Sockets</code>.
   */
  private ClientSocketFactory clientSocketFactory;

  /**
   * Whether to enable TCP keep alive for sockets. This boolean is controlled by
   * the gemfire.setTcpKeepAlive java system property.  If not set then GemFire
   * will enable keep-alive on server->client and p2p connections.
   */
  public static final boolean ENABLE_TCP_KEEP_ALIVE;

  static {
    // bug #49484 - customers want tcp/ip keep-alive turned on by default
    // to avoid dropped connections.  It can be turned off by setting this
    // property to false
    String str = System.getProperty(DistributionConfig.GEMFIRE_PREFIX + "setTcpKeepAlive");
    if (str != null) {
      ENABLE_TCP_KEEP_ALIVE = Boolean.valueOf(str);
    } else {
      ENABLE_TCP_KEEP_ALIVE = true;
    }
  }

  // -------------------------------------------------------------------------
  //   Constructor
  // -------------------------------------------------------------------------

  /**
   * Constructs new SocketCreator instance.
   */
  SocketCreator(final SSLConfig sslConfig) {
    this.sslConfig = sslConfig;
    initialize();
  }


  // -------------------------------------------------------------------------
  //   Static instance accessors
  // -------------------------------------------------------------------------

  /**
   * All GemFire code should use this method instead of
   * InetAddress.getLocalHost().  See bug #40619
   */
  public static InetAddress getLocalHost() throws UnknownHostException {
    if (localHost == null) {
      throw new UnknownHostException();
    }
    return localHost;
  }

  /**
   * All classes should use this instead of relying on the JRE system property
   */
  public static boolean preferIPv6Addresses() {
    return SocketCreator.useIPv6Addresses;
  }

  /**
   * returns the host name for the given inet address, using a local cache
   * of names to avoid dns hits and duplicate strings
   */
  public static synchronized String getHostName(InetAddress addr) {
    String result = (String) hostNames.get(addr);
    if (result == null) {
      result = addr.getHostName();
      hostNames.put(addr, result);
    }
    return result;
  }

  /**
   * returns the host name for the given inet address, using a local cache
   * of names to avoid dns hits and duplicate strings
   */
  public static synchronized String getCanonicalHostName(InetAddress addr, String hostName) {
    String result = (String) hostNames.get(addr);
    if (result == null) {
      hostNames.put(addr, hostName);
      return hostName;
    }
    return result;
  }

  /**
   * Reset the hostNames caches
   */
  public static synchronized void resetHostNameCache() {
    hostNames.clear();
  }

  // -------------------------------------------------------------------------
  //   Initializers (change SocketCreator state)
  // -------------------------------------------------------------------------

  /**
   * Initialize this SocketCreator.
   * <p>
   * Caller must synchronize on the SocketCreator instance.
   */
  @SuppressWarnings("hiding")
  private void initialize() {
    try {
      // set p2p values...
      if (SecurableCommunicationChannel.CLUSTER.equals(sslConfig.getSecuredCommunicationChannel())) {
        if (this.sslConfig.isEnabled()) {
          System.setProperty("p2p.useSSL", "true");
          System.setProperty("p2p.oldIO", "true");
          System.setProperty("p2p.nodirectBuffers", "true");
        } else {
          System.setProperty("p2p.useSSL", "false");
        }
      }

      try {
        if (this.sslConfig.isEnabled() && sslContext == null) {
          sslContext = createAndConfigureSSLContext();
          SSLContext.setDefault(sslContext);
        }
      } catch (Exception e) {
        throw new GemFireConfigException("Error configuring GemFire ssl ", e);
      }

      // make sure TCPConduit picks up p2p properties...
      org.apache.geode.internal.tcp.TCPConduit.init();

      initializeClientSocketFactory();
      this.ready = true;
    } catch (VirtualMachineError err) {
      SystemFailure.initiateFailure(err);
      // If this ever returns, rethrow the error.  We're poisoned
      // now, so don't let this thread continue.
      throw err;
    } catch (Error t) {
      // Whenever you catch Error or Throwable, you must also
      // catch VirtualMachineError (see above).  However, there is
      // _still_ a possibility that you are dealing with a cascading
      // error condition, so you also need to check to see if the JVM
      // is still usable:
      SystemFailure.checkFailure();
      t.printStackTrace();
      throw t;
    } catch (RuntimeException re) {
      re.printStackTrace();
      throw re;
    }
  }

  /**
   * Creates & configures the SSLContext when SSL is enabled.
   * @return new SSLContext configured using the given protocols & properties
   *
   * @throws GeneralSecurityException if security information can not be found
   * @throws IOException if information can not be loaded
   */
  private SSLContext createAndConfigureSSLContext() throws GeneralSecurityException, IOException {

    SSLContext newSSLContext = getSSLContextInstance();
    KeyManager[] keyManagers = getKeyManagers();
    TrustManager[] trustManagers = getTrustManagers();

    newSSLContext.init(keyManagers, trustManagers, null /* use the default secure random*/);
    return newSSLContext;
  }

  /**
   * Used by CacheServerLauncher and SystemAdmin to read the properties from
   * console
   * @param env Map in which the properties are to be read from console.
   */
  public static void readSSLProperties(Map<String, String> env) {
    readSSLProperties(env, false);
  }

  /**
   * Used to read the properties from console. AgentLauncher calls this method
   * directly & ignores gemfire.properties. CacheServerLauncher and SystemAdmin
   * call this through {@link #readSSLProperties(Map)} and do NOT ignore
   * gemfire.properties.
   * @param env Map in which the properties are to be read from console.
   * @param ignoreGemFirePropsFile if <code>false</code> existing gemfire.properties file is read, if
   * <code>true</code>, properties from gemfire.properties file are
   * ignored.
   */
  public static void readSSLProperties(Map<String, String> env, boolean ignoreGemFirePropsFile) {
    Properties props = new Properties();
    DistributionConfigImpl.loadGemFireProperties(props, ignoreGemFirePropsFile);
    for (Object entry : props.entrySet()) {
      Map.Entry<String, String> ent = (Map.Entry<String, String>) entry;
      // if the value of ssl props is empty, read them from console
      if (ent.getKey().startsWith(DistributionConfig.SSL_SYSTEM_PROPS_NAME) || ent.getKey().startsWith(DistributionConfig.SYS_PROP_NAME)) {
        String key = ent.getKey();
        if (key.startsWith(DistributionConfig.SYS_PROP_NAME)) {
          key = key.substring(DistributionConfig.SYS_PROP_NAME.length());
        }
        if (ent.getValue() == null || ent.getValue().trim().equals("")) {
          GfeConsoleReader consoleReader = GfeConsoleReaderFactory.getDefaultConsoleReader();
          if (!consoleReader.isSupported()) {
            throw new GemFireConfigException("SSL properties are empty, but a console is not available");
          }
          if (key.toLowerCase().contains("password")) {
            char[] password = consoleReader.readPassword("Please enter " + key + ": ");
            env.put(key, PasswordUtil.encrypt(new String(password), false));
          } else {
            String val = consoleReader.readLine("Please enter " + key + ": ");
            env.put(key, val);
          }

        }
      }
    }
  }

  private SSLContext getSSLContextInstance() {
    String[] protocols = sslConfig.getProtocolsAsStringArray();
    SSLContext sslContext = null;
    if (protocols != null && protocols.length > 0) {
      for (String protocol : protocols) {
        if (!protocol.equals("any")) {
          try {
            sslContext = SSLContext.getInstance(protocol);
            break;
          } catch (NoSuchAlgorithmException e) {
            // continue
          }
        }
      }
    }
    if (sslContext != null) {
      return sslContext;
    }
    // lookup known algorithms
    String[] knownAlgorithms = { "SSL", "SSLv2", "SSLv3", "TLS", "TLSv1", "TLSv1.1", "TLSv1.2" };
    for (String algo : knownAlgorithms) {
      try {
        sslContext = SSLContext.getInstance(algo);
        break;
      } catch (NoSuchAlgorithmException e) {
        // continue
      }
    }
    return sslContext;
  }

  private TrustManager[] getTrustManagers() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
    TrustManager[] trustManagers = null;
    GfeConsoleReader consoleReader = GfeConsoleReaderFactory.getDefaultConsoleReader();

    String trustStoreType = sslConfig.getTruststoreType();
    if (StringUtils.isEmpty(trustStoreType)) {
      //read from console, default on empty
      if (consoleReader.isSupported()) {
        trustStoreType = consoleReader.readLine("Please enter the trustStoreType (javax.net.ssl.trustStoreType) : ");
      } else {
        trustStoreType = KeyStore.getDefaultType();
      }
    }

    KeyStore ts = KeyStore.getInstance(trustStoreType);
    String trustStorePath = sslConfig.getTruststore();
    if (StringUtils.isEmpty(trustStorePath)) {
      if (consoleReader.isSupported()) {
        trustStorePath = consoleReader.readLine("Please enter the trustStore location (javax.net.ssl.trustStore) : ");
      }
    }
    FileInputStream fis = new FileInputStream(trustStorePath);
    String passwordString = sslConfig.getTruststorePassword();
    char[] password = null;
    if (passwordString != null) {
      if (passwordString.trim().equals("")) {
        if (!StringUtils.isEmpty(passwordString)) {
          String toDecrypt = "encrypted(" + passwordString + ")";
          passwordString = PasswordUtil.decrypt(toDecrypt);
          password = passwordString.toCharArray();
        }
        //read from the console
        if (StringUtils.isEmpty(passwordString) && consoleReader.isSupported()) {
          password = consoleReader.readPassword("Please enter password for trustStore (javax.net.ssl.trustStorePassword) : ");
        }
      } else {
        password = passwordString.toCharArray();
      }
    }
    ts.load(fis, password);

    // default algorithm can be changed by setting property "ssl.TrustManagerFactory.algorithm" in security properties
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ts);
    trustManagers = tmf.getTrustManagers();
    // follow the security tip in java doc
    if (password != null) {
      java.util.Arrays.fill(password, ' ');
    }

    return trustManagers;
  }

  private KeyManager[] getKeyManagers() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
    GfeConsoleReader consoleReader = GfeConsoleReaderFactory.getDefaultConsoleReader();

    KeyManager[] keyManagers = null;
    String keyStoreType = sslConfig.getKeystoreType();
    if (StringUtils.isEmpty(keyStoreType)) {
      // read from console, default on empty
      if (consoleReader.isSupported()) {
        keyStoreType = consoleReader.readLine("Please enter the keyStoreType (javax.net.ssl.keyStoreType) : ");
      } else {
        keyStoreType = KeyStore.getDefaultType();
      }
    }
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    String keyStoreFilePath = sslConfig.getKeystore();
    if (StringUtils.isEmpty(keyStoreFilePath)) {
      if (consoleReader.isSupported()) {
        keyStoreFilePath = consoleReader.readLine("Please enter the keyStore location (javax.net.ssl.keyStore) : ");
      } else {
        keyStoreFilePath = System.getProperty("user.home") + System.getProperty("file.separator") + ".keystore";
      }
    }

    FileInputStream fileInputStream = new FileInputStream(keyStoreFilePath);
    String passwordString = sslConfig.getKeystorePassword();
    char[] password = null;
    if (passwordString != null) {
      if (passwordString.trim().equals("")) {
        String encryptedPass = System.getenv("javax.net.ssl.keyStorePassword");
        if (!StringUtils.isEmpty(encryptedPass)) {
          String toDecrypt = "encrypted(" + encryptedPass + ")";
          passwordString = PasswordUtil.decrypt(toDecrypt);
          password = passwordString.toCharArray();
        }
        //read from the console
        if (StringUtils.isEmpty(passwordString) && consoleReader != null) {
          password = consoleReader.readPassword("Please enter password for keyStore (javax.net.ssl.keyStorePassword) : ");
        }
      } else {
        password = passwordString.toCharArray();
      }
    }
    keyStore.load(fileInputStream, password);
    // default algorithm can be changed by setting property "ssl.KeyManagerFactory.algorithm" in security properties
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, password);
    keyManagers = keyManagerFactory.getKeyManagers();
    // follow the security tip in java doc
    if (password != null) {
      java.util.Arrays.fill(password, ' ');
    }

    KeyManager[] extendedKeyManagers = new KeyManager[keyManagers.length];

    for (int i = 0; i < keyManagers.length; i++)

    {
      extendedKeyManagers[i] = new ExtendedAliasKeyManager(keyManagers[i], sslConfig.getAlias());
    }

    return extendedKeyManagers;
  }

  private static final class ExtendedAliasKeyManager extends X509ExtendedKeyManager {

    private final X509ExtendedKeyManager delegate;

    private final String keyAlias;

    /**
     * Constructor.
     * @param mgr The X509KeyManager used as a delegate
     * @param keyAlias The alias name of the server's keypair and supporting
     * certificate chain
     */
    ExtendedAliasKeyManager(KeyManager mgr, String keyAlias) {
      this.delegate = (X509ExtendedKeyManager) mgr;
      this.keyAlias = keyAlias;
    }


    @Override
    public String[] getClientAliases(final String s, final Principal[] principals) {
      return delegate.getClientAliases(s, principals);
    }

    @Override
    public String chooseClientAlias(final String[] strings, final Principal[] principals, final Socket socket) {
      if (!StringUtils.isEmpty(this.keyAlias)) {
        return keyAlias;
      }
      return delegate.chooseClientAlias(strings, principals, socket);
    }

    @Override
    public String[] getServerAliases(final String s, final Principal[] principals) {
      return delegate.getServerAliases(s, principals);
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
      if (!StringUtils.isEmpty(this.keyAlias)) {
        PrivateKey key = this.delegate.getPrivateKey(this.keyAlias);
        return getKeyAlias(keyType, key);
      }
      return this.delegate.chooseServerAlias(keyType, issuers, socket);

    }

    @Override
    public X509Certificate[] getCertificateChain(final String s) {
      if (!StringUtils.isEmpty(this.keyAlias)) {
        return delegate.getCertificateChain(keyAlias);
      }
      return delegate.getCertificateChain(s);
    }

    @Override
    public PrivateKey getPrivateKey(final String alias) {
      return delegate.getPrivateKey(alias);
    }

    @Override
    public String chooseEngineServerAlias(final String keyType, final Principal[] principals, final SSLEngine sslEngine) {
      if (!StringUtils.isEmpty(this.keyAlias)) {
        PrivateKey key = this.delegate.getPrivateKey(this.keyAlias);
        return getKeyAlias(keyType, key);
      }
      return this.delegate.chooseEngineServerAlias(keyType, principals, sslEngine);

    }

    private String getKeyAlias(final String keyType, final PrivateKey key) {
      if (key != null) {
        if (key.getAlgorithm().equals(keyType)) {
          return this.keyAlias;
        } else {
          return null;
        }
      } else {
        return null;
      }
    }
  }

  // -------------------------------------------------------------------------
  //   Public methods
  // -------------------------------------------------------------------------

  /**
   * Returns true if this SocketCreator is configured to use SSL.
   */
  public boolean useSSL() {
    return this.sslConfig.isEnabled();
  }

  /**
   * Return a ServerSocket possibly configured for SSL.
   * SSL configuration is left up to JSSE properties in java.security file.
   */
  public ServerSocket createServerSocket(int nport, int backlog) throws IOException {
    return createServerSocket(nport, backlog, null);
  }

  public ServerSocket createServerSocket(int nport, int backlog, InetAddress bindAddr, List<GatewayTransportFilter> transportFilters, int socketBufferSize)
    throws IOException {
    if (transportFilters.isEmpty()) {
      return createServerSocket(nport, backlog, bindAddr, socketBufferSize);
    } else {
      printConfig();
      ServerSocket result = new TransportFilterServerSocket(transportFilters);
      result.setReuseAddress(true);
      // Set the receive buffer size before binding the socket so
      // that large buffers will be allocated on accepted sockets (see
      // java.net.ServerSocket.setReceiverBufferSize javadocs)
      result.setReceiveBufferSize(socketBufferSize);
      try {
        result.bind(new InetSocketAddress(bindAddr, nport), backlog);
      } catch (BindException e) {
        BindException throwMe = new BindException(LocalizedStrings.SocketCreator_FAILED_TO_CREATE_SERVER_SOCKET_ON_0_1.toLocalizedString(new Object[] {
          bindAddr, Integer.valueOf(nport)
        }));
        throwMe.initCause(e);
        throw throwMe;
      }
      return result;
    }
  }

  /**
   * Return a ServerSocket possibly configured for SSL.
   * SSL configuration is left up to JSSE properties in java.security file.
   */
  public ServerSocket createServerSocket(int nport, int backlog, InetAddress bindAddr) throws IOException {
    return createServerSocket(nport, backlog, bindAddr, -1, sslConfig.isEnabled());
  }

  public ServerSocket createServerSocket(int nport, int backlog, InetAddress bindAddr, int socketBufferSize) throws IOException {
    return createServerSocket(nport, backlog, bindAddr, socketBufferSize, sslConfig.isEnabled());
  }

  private ServerSocket createServerSocket(int nport, int backlog, InetAddress bindAddr, int socketBufferSize, boolean sslConnection) throws IOException {
    printConfig();
    if (sslConnection) {
      if (this.sslContext == null) {
        throw new GemFireConfigException("SSL not configured correctly, Please look at previous error");
      }
      ServerSocketFactory ssf = this.sslContext.getServerSocketFactory();
      SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket();
      serverSocket.setReuseAddress(true);
      // If necessary, set the receive buffer size before binding the socket so
      // that large buffers will be allocated on accepted sockets (see
      // java.net.ServerSocket.setReceiverBufferSize javadocs)
      if (socketBufferSize != -1) {
        serverSocket.setReceiveBufferSize(socketBufferSize);
      }
      serverSocket.bind(new InetSocketAddress(bindAddr, nport), backlog);
      finishServerSocket(serverSocket);
      return serverSocket;
    } else {
      //log.info("Opening server socket on " + nport, new Exception("SocketCreation"));
      ServerSocket result = new ServerSocket();
      result.setReuseAddress(true);
      // If necessary, set the receive buffer size before binding the socket so
      // that large buffers will be allocated on accepted sockets (see
      // java.net.ServerSocket.setReceiverBufferSize javadocs)
      if (socketBufferSize != -1) {
        result.setReceiveBufferSize(socketBufferSize);
      }
      try {
        result.bind(new InetSocketAddress(bindAddr, nport), backlog);
      } catch (BindException e) {
        BindException throwMe = new BindException(LocalizedStrings.SocketCreator_FAILED_TO_CREATE_SERVER_SOCKET_ON_0_1.toLocalizedString(new Object[] {
          bindAddr, Integer.valueOf(nport)
        }));
        throwMe.initCause(e);
        throw throwMe;
      }
      return result;
    }
  }

  /**
   * Creates or bind server socket to a random port selected
   * from tcp-port-range which is same as membership-port-range.
   * @param ba
   * @param backlog
   * @param isBindAddress
   * @param tcpBufferSize
   *
   * @return Returns the new server socket.
   *
   * @throws IOException
   */
  public ServerSocket createServerSocketUsingPortRange(InetAddress ba,
                                                       int backlog,
                                                       boolean isBindAddress,
                                                       boolean useNIO,
                                                       int tcpBufferSize,
                                                       int[] tcpPortRange) throws IOException {
    return createServerSocketUsingPortRange(ba, backlog, isBindAddress, useNIO, tcpBufferSize, tcpPortRange, sslConfig.isEnabled());
  }

  /**
   * Creates or bind server socket to a random port selected
   * from tcp-port-range which is same as membership-port-range.
   * @param ba
   * @param backlog
   * @param isBindAddress
   * @param tcpBufferSize
   * @param sslConnection whether to connect using SSL
   *
   * @return Returns the new server socket.
   *
   * @throws IOException
   */
  public ServerSocket createServerSocketUsingPortRange(InetAddress ba,
                                                       int backlog,
                                                       boolean isBindAddress,
                                                       boolean useNIO,
                                                       int tcpBufferSize,
                                                       int[] tcpPortRange,
                                                       boolean sslConnection) throws IOException {
    ServerSocket socket = null;
    int localPort = 0;
    int startingPort = 0;

    // Get a random port from range.
    Random rand = new SecureRandom();
    int portLimit = tcpPortRange[1];
    int randPort = tcpPortRange[0] + rand.nextInt(tcpPortRange[1] - tcpPortRange[0] + 1);

    startingPort = randPort;
    localPort = startingPort;

    while (true) {
      if (localPort > portLimit) {
        if (startingPort != 0) {
          localPort = tcpPortRange[0];
          portLimit = startingPort - 1;
          startingPort = 0;
        } else {
          throw new SystemConnectException(LocalizedStrings.TCPConduit_UNABLE_TO_FIND_FREE_PORT.toLocalizedString());
        }
      }
      try {
        if (useNIO) {
          ServerSocketChannel channl = ServerSocketChannel.open();
          socket = channl.socket();

          InetSocketAddress addr = new InetSocketAddress(isBindAddress ? ba : null, localPort);
          socket.bind(addr, backlog);
        } else {
          socket = this.createServerSocket(localPort, backlog, isBindAddress ? ba : null, tcpBufferSize, sslConnection);
        }
        break;
      } catch (java.net.SocketException ex) {
        if (useNIO || SocketCreator.treatAsBindException(ex)) {
          localPort++;
        } else {
          throw ex;
        }
      }
    }
    return socket;
  }

  public static boolean treatAsBindException(SocketException se) {
    if (se instanceof BindException) {
      return true;
    }
    final String msg = se.getMessage();
    return (msg != null && msg.contains("Invalid argument: listen failed"));
  }

  /**
   * Return a client socket. This method is used by client/server clients.
   */
  public Socket connectForClient(String host, int port, int timeout) throws IOException {
    return connect(InetAddress.getByName(host), port, timeout, null, true, -1);
  }

  /**
   * Return a client socket. This method is used by client/server clients.
   */
  public Socket connectForClient(String host, int port, int timeout, int socketBufferSize) throws IOException {
    return connect(InetAddress.getByName(host), port, timeout, null, true, socketBufferSize);
  }

  /**
   * Return a client socket. This method is used by peers.
   */
  public Socket connectForServer(InetAddress inetadd, int port) throws IOException {
    return connect(inetadd, port, 0, null, false, -1);
  }

  /**
   * Return a client socket. This method is used by peers.
   */
  public Socket connectForServer(InetAddress inetadd, int port, int socketBufferSize) throws IOException {
    return connect(inetadd, port, 0, null, false, socketBufferSize);
  }

  /**
   * Return a client socket, timing out if unable to connect and timeout > 0 (millis).
   * The parameter <i>timeout</i> is ignored if SSL is being used, as there is no
   * timeout argument in the ssl socket factory
   */
  public Socket connect(InetAddress inetadd, int port, int timeout, ConnectionWatcher optionalWatcher, boolean clientSide) throws IOException {
    return connect(inetadd, port, timeout, optionalWatcher, clientSide, -1);
  }

  /**
   * Return a client socket, timing out if unable to connect and timeout > 0 (millis).
   * The parameter <i>timeout</i> is ignored if SSL is being used, as there is no
   * timeout argument in the ssl socket factory
   */
  public Socket connect(InetAddress inetadd, int port, int timeout, ConnectionWatcher optionalWatcher, boolean clientSide, int socketBufferSize)
    throws IOException {
    return connect(inetadd, port, timeout, optionalWatcher, clientSide, socketBufferSize, sslConfig.isEnabled());
  }

  /**
   * Return a client socket, timing out if unable to connect and timeout > 0 (millis).
   * The parameter <i>timeout</i> is ignored if SSL is being used, as there is no
   * timeout argument in the ssl socket factory
   */
  public Socket connect(InetAddress inetadd,
                        int port,
                        int timeout,
                        ConnectionWatcher optionalWatcher,
                        boolean clientSide,
                        int socketBufferSize,
                        boolean sslConnection) throws IOException {
    Socket socket = null;
    SocketAddress sockaddr = new InetSocketAddress(inetadd, port);
    printConfig();
    try {
      if (sslConnection) {
        if (this.sslContext == null) {
          throw new GemFireConfigException("SSL not configured correctly, Please look at previous error");
        }
        SocketFactory sf = this.sslContext.getSocketFactory();
        socket = sf.createSocket();

        // Optionally enable SO_KEEPALIVE in the OS network protocol.
        socket.setKeepAlive(ENABLE_TCP_KEEP_ALIVE);

        // If necessary, set the receive buffer size before connecting the
        // socket so that large buffers will be allocated on accepted sockets
        // (see java.net.Socket.setReceiverBufferSize javadocs for details)
        if (socketBufferSize != -1) {
          socket.setReceiveBufferSize(socketBufferSize);
        }

        if (optionalWatcher != null) {
          optionalWatcher.beforeConnect(socket);
        }
        socket.connect(sockaddr, Math.max(timeout, 0));
        configureClientSSLSocket(socket);
        return socket;
      } else {
        if (clientSide && this.clientSocketFactory != null) {
          socket = this.clientSocketFactory.createSocket(inetadd, port);
        } else {
          socket = new Socket();

          // Optionally enable SO_KEEPALIVE in the OS network protocol.
          socket.setKeepAlive(ENABLE_TCP_KEEP_ALIVE);

          // If necessary, set the receive buffer size before connecting the
          // socket so that large buffers will be allocated on accepted sockets
          // (see java.net.Socket.setReceiverBufferSize javadocs for details)
          if (socketBufferSize != -1) {
            socket.setReceiveBufferSize(socketBufferSize);
          }

          if (optionalWatcher != null) {
            optionalWatcher.beforeConnect(socket);
          }
          socket.connect(sockaddr, Math.max(timeout, 0));
        }
        return socket;
      }
    } finally {
      if (optionalWatcher != null) {
        optionalWatcher.afterConnect(socket);
      }
    }
  }

  /**
   * Will be a server socket... this one simply registers the listeners.
   */
  public void configureServerSSLSocket(Socket socket) throws IOException {
    if (socket instanceof SSLSocket) {
      SSLSocket sslSocket = (SSLSocket) socket;
      try {
        sslSocket.startHandshake();
        SSLSession session = sslSocket.getSession();
        Certificate[] peer = session.getPeerCertificates();
        if (logger.isDebugEnabled()) {
          logger.debug(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_CONNECTION_FROM_PEER_0, ((X509Certificate) peer[0]).getSubjectDN()));
        }
      } catch (SSLPeerUnverifiedException ex) {
        if (this.sslConfig.isRequireAuth()) {
          logger.fatal(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_ERROR_IN_AUTHENTICATING_PEER_0_1, new Object[] {
            socket.getInetAddress(), Integer.valueOf(socket.getPort())
          }), ex);
          throw ex;
        }
      } catch (SSLException ex) {
        logger.fatal(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_ERROR_IN_CONNECTING_TO_PEER_0_1, new Object[] {
          socket.getInetAddress(), Integer.valueOf(socket.getPort())
        }), ex);
        throw ex;
      }
    }
  }

  // -------------------------------------------------------------------------
  //   Private implementation methods
  // -------------------------------------------------------------------------

  /**
   * Configure the SSLServerSocket based on this SocketCreator's settings.
   */
  private void finishServerSocket(SSLServerSocket serverSocket) throws IOException {
    serverSocket.setUseClientMode(false);
    if (this.sslConfig.isRequireAuth()) {
      //serverSocket.setWantClientAuth( true );
      serverSocket.setNeedClientAuth(true);
    }
    serverSocket.setEnableSessionCreation(true);

    // restrict protocols
    String[] protocols = this.sslConfig.getProtocolsAsStringArray();
    if (!"any".equalsIgnoreCase(protocols[0])) {
      serverSocket.setEnabledProtocols(protocols);
    }
    // restrict ciphers
    String[] ciphers = this.sslConfig.getCiphersAsStringArray();
    if (!"any".equalsIgnoreCase(ciphers[0])) {
      serverSocket.setEnabledCipherSuites(ciphers);
    }
  }

  /**
   * When a socket is accepted from a server socket, it should be passed to
   * this method for SSL configuration.
   */
  private void configureClientSSLSocket(Socket socket) throws IOException {
    if (socket instanceof SSLSocket) {
      SSLSocket sslSocket = (SSLSocket) socket;

      sslSocket.setUseClientMode(true);
      sslSocket.setEnableSessionCreation(true);

      String[] protocols = this.sslConfig.getProtocolsAsStringArray();

      // restrict cyphers
      if (protocols != null && !"any".equalsIgnoreCase(protocols[0])) {
        sslSocket.setEnabledProtocols(protocols);
      }
      String[] ciphers = this.sslConfig.getCiphersAsStringArray();
      if (ciphers != null && !"any".equalsIgnoreCase(ciphers[0])) {
        sslSocket.setEnabledCipherSuites(ciphers);
      }

      try {
        sslSocket.startHandshake();
        SSLSession session = sslSocket.getSession();
        Certificate[] peer = session.getPeerCertificates();
        if (logger.isDebugEnabled()) {
          logger.debug(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_CONNECTION_FROM_PEER_0, ((X509Certificate) peer[0]).getSubjectDN()));
        }
      } catch (SSLHandshakeException ex) {
        logger.fatal(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_ERROR_IN_CONNECTING_TO_PEER_0_1, new Object[] {
          socket.getInetAddress(), Integer.valueOf(socket.getPort())
        }), ex);
        throw ex;
      } catch (SSLPeerUnverifiedException ex) {
        if (this.sslConfig.isRequireAuth()) {
          logger.fatal(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_ERROR_IN_AUTHENTICATING_PEER), ex);
          throw ex;
        }
      } catch (SSLException ex) {
        logger.fatal(LocalizedMessage.create(LocalizedStrings.SocketCreator_SSL_ERROR_IN_CONNECTING_TO_PEER_0_1, new Object[] {
          socket.getInetAddress(), Integer.valueOf(socket.getPort())
        }), ex);
        throw ex;
      }

    }
  }

  /**
   * Print current configured state to log.
   */
  private void printConfig() {
    if (!configShown && logger.isDebugEnabled()) {
      configShown = true;
      StringBuffer sb = new StringBuffer();
      sb.append("SSL Configuration: \n");
      sb.append("  ssl-enabled = " + this.sslConfig.isEnabled()).append("\n");
      // add other options here....
      for (String key : System.getProperties().stringPropertyNames()) { // fix for 46822
        if (key.startsWith("javax.net.ssl")) {
          sb.append("  ").append(key).append(" = ").append(System.getProperty(key)).append("\n");
        }
      }
      logger.debug(sb.toString());
    }
  }


  protected void initializeClientSocketFactory() {
    this.clientSocketFactory = null;
    String className = System.getProperty(DistributionConfig.GEMFIRE_PREFIX + "clientSocketFactory");
    if (className != null) {
      Object o;
      try {
        Class c = ClassPathLoader.getLatest().forName(className);
        o = c.newInstance();
      } catch (Exception e) {
        // No cache exists yet, so this can't be logged.
        String s = "An unexpected exception occurred while instantiating a " + className + ": " + e;
        throw new IllegalArgumentException(s);
      }
      if (o instanceof ClientSocketFactory) {
        this.clientSocketFactory = (ClientSocketFactory) o;
      } else {
        String s = "Class \"" + className + "\" is not a ClientSocketFactory";
        throw new IllegalArgumentException(s);
      }
    }
  }

  public void initializeTransportFilterClientSocketFactory(GatewaySender sender) {
    this.clientSocketFactory = new TransportFilterSocketFactory().setGatewayTransportFilters(sender.getGatewayTransportFilters());
  }

  /**
   * returns a set of the non-loopback InetAddresses for this machine
   */
  public static Set<InetAddress> getMyAddresses() {
    Set<InetAddress> result = new HashSet<InetAddress>();
    Set<InetAddress> locals = new HashSet<InetAddress>();
    Enumeration<NetworkInterface> interfaces;
    try {
      interfaces = NetworkInterface.getNetworkInterfaces();
    } catch (SocketException e) {
      throw new IllegalArgumentException(LocalizedStrings.StartupMessage_UNABLE_TO_EXAMINE_NETWORK_INTERFACES.toLocalizedString(), e);
    }
    while (interfaces.hasMoreElements()) {
      NetworkInterface face = interfaces.nextElement();
      boolean faceIsUp = false;
      try {
        faceIsUp = face.isUp();
      } catch (SocketException e) {
        InternalDistributedSystem ids = InternalDistributedSystem.getAnyInstance();
        if (ids != null) {
          logger.info("Failed to check if network interface is up. Skipping {}", face, e);
        }
      }
      if (faceIsUp) {
        Enumeration<InetAddress> addrs = face.getInetAddresses();
        while (addrs.hasMoreElements()) {
          InetAddress addr = addrs.nextElement();
          if (addr.isLoopbackAddress() || addr.isAnyLocalAddress() || (!useLinkLocalAddresses && addr.isLinkLocalAddress())) {
            locals.add(addr);
          } else {
            result.add(addr);
          }
        } // while
      }
    } // while
    // fix for bug #42427 - allow product to run on a standalone box by using
    // local addresses if there are no non-local addresses available
    if (result.size() == 0) {
      return locals;
    } else {
      return result;
    }
  }

  /**
   * This method uses JNDI to look up an address in DNS and return its name
   * @param addr
   *
   * @return the host name associated with the address or null if lookup isn't possible or there is no host name for this address
   */
  public static String reverseDNS(InetAddress addr) {
    byte[] addrBytes = addr.getAddress();
    // reverse the address suitable for reverse lookup
    String lookup = "";
    for (int index = addrBytes.length - 1; index >= 0; index--) {
      lookup = lookup + (addrBytes[index] & 0xff) + '.';
    }
    lookup += "in-addr.arpa";
    //    System.out.println("Looking up: " + lookup);

    try {
      Hashtable env = new Hashtable();
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
      DirContext ctx = new InitialDirContext(env);
      Attributes attrs = ctx.getAttributes(lookup, new String[] { "PTR" });
      for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements(); ) {
        Attribute attr = (Attribute) ae.next();
        for (Enumeration vals = attr.getAll(); vals.hasMoreElements(); ) {
          Object elem = vals.nextElement();
          if ("PTR".equals(attr.getID()) && elem != null) {
            return elem.toString();
          }
        }
      }
      ctx.close();
    } catch (Exception e) {
      // ignored
    }
    return null;
  }

  /**
   * Returns true if host matches the LOCALHOST.
   */
  public static boolean isLocalHost(Object host) {
    if (host instanceof InetAddress) {
      if (InetAddressUtil.LOCALHOST.equals(host)) {
        return true;
      } else if (((InetAddress) host).isLoopbackAddress()) {
        return true;
      } else {
        try {
          Enumeration en = NetworkInterface.getNetworkInterfaces();
          while (en.hasMoreElements()) {
            NetworkInterface i = (NetworkInterface) en.nextElement();
            for (Enumeration en2 = i.getInetAddresses(); en2.hasMoreElements(); ) {
              InetAddress addr = (InetAddress) en2.nextElement();
              if (host.equals(addr)) {
                return true;
              }
            }
          }
          return false;
        } catch (SocketException e) {
          throw new IllegalArgumentException(LocalizedStrings.InetAddressUtil_UNABLE_TO_QUERY_NETWORK_INTERFACE.toLocalizedString(), e);
        }
      }
    } else {
      return isLocalHost(toInetAddress(host.toString()));
    }
  }

  /**
   * Converts the string host to an instance of InetAddress.  Returns null if
   * the string is empty.  Fails Assertion if the conversion would result in
   * <code>java.lang.UnknownHostException</code>.
   * <p>
   * Any leading slashes on host will be ignored.
   * @param host string version the InetAddress
   *
   * @return the host converted to InetAddress instance
   */
  public static InetAddress toInetAddress(String host) {
    if (host == null || host.length() == 0) {
      return null;
    }
    try {
      if (host.indexOf("/") > -1) {
        return InetAddress.getByName(host.substring(host.indexOf("/") + 1));
      } else {
        return InetAddress.getByName(host);
      }
    } catch (java.net.UnknownHostException e) {
      throw new IllegalArgumentException(e.getMessage());
    }
  }
}

